package com.redxiii.tracplus.web.servlets;

import java.io.IOException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.lucene.search.Query;
import org.apache.velocity.VelocityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.redxiii.tracplus.ejb.search.SearchManager;
import com.redxiii.tracplus.ejb.search.SearchManagerFactory;
import com.redxiii.tracplus.ejb.search.SearchResult;
import com.redxiii.tracplus.ejb.search.TracStuffField;
import com.redxiii.tracplus.ejb.search.query.QueryBuilder;
import com.redxiii.tracplus.ejb.search.query.SimpleQuerySpec;
import com.redxiii.tracplus.ejb.util.AppConfiguration;
import com.redxiii.tracplus.ejb.util.AppConfiguration.AppParam;
import com.redxiii.tracplus.web.util.Credentials;
import com.redxiii.tracplus.web.util.ManifestReader;
import com.redxiii.tracplus.web.util.TemplateUtility;

public class SearchServlet extends HttpServlet {
	
	private static final Logger logger = LoggerFactory.getLogger(SearchServlet.class);
	private static final long serialVersionUID = 1L;
	
	private static enum FILTER_TYPE_SELECTION {
		both,
		ticket,
		wiki,
	}
	
	private static enum RECENT_TIME_SELECTION { 
		all_entries(0),
		last_week(7),
		last_month(30),
		last_quarter(90),
		last_halfyear(180),
		last_year(360),
		;
		int days;
		private RECENT_TIME_SELECTION(int days) {
			this.days = days;
		}
	}
	
	private static enum SORT_SELECTION { 
		relevance,
		date,
		magic,
	}
	
	private static enum SEARCH_TYPE {
		normal_search,
		fast_search,
		lucene_search,
	}
	
	private static String manifest; 
	private TemplateUtility templateUtility;
	private boolean verifyCredentials;
	private Credentials credentials;
	private String userid;
	
	private RECENT_TIME_SELECTION recentTimeSelection;
	private SORT_SELECTION sortSelection;
	private FILTER_TYPE_SELECTION filterTypeSelection;
	private SEARCH_TYPE searchType;
	private String searchInfo;
	private long requestTimestamp;
	
	public SearchServlet() {
		verifyCredentials = AppConfiguration.getInstance().getAsBoolean(AppParam.CREDENTIAL_REQUIRED, false);
	}
	
	private void fetchPageParameters(HttpServletRequest request) {
		
		recentTimeSelection = RECENT_TIME_SELECTION.all_entries;
		sortSelection = SORT_SELECTION.relevance;
		filterTypeSelection = FILTER_TYPE_SELECTION.both;
		searchType = SEARCH_TYPE.normal_search;
		
		{
			String param = request.getParameter("recent-time");
			if (param != null && param.length() > 0) {
				recentTimeSelection = RECENT_TIME_SELECTION.valueOf(param);
			}
		}
		{
			String param = request.getParameter("sortby");
			if (param != null && param.length() > 0) {
				sortSelection = SORT_SELECTION.valueOf(param);
			}
		}
		{
			String param = request.getParameter("filter-type");
			if (param != null && param.length() > 0) {
				filterTypeSelection = FILTER_TYPE_SELECTION.valueOf(param);
			}
		}
		{
			for (SEARCH_TYPE type : SEARCH_TYPE.values()) {
				String param = request.getParameter(type.name());
				if (param != null && param.equals(type.name())) {
					searchType = type;
					break;
				}
			}
		}
		searchInfo = request.getParameter("value");
	}
	
	private static final Comparator<SearchResult> SORT_SEARCHS_BY_DATE = new Comparator<SearchResult>() {
		@Override
		public int compare(SearchResult o1, SearchResult o2) {
			if (o1.getModifiedDate() == null || o2.getModifiedDate() == null)
				return 1;
			if (o1.getModifiedDate().equals(o2.getModifiedDate()))
				return Float.compare(o2.getScore(), o1.getScore());
			return o2.getModifiedDate().compareTo(o1.getModifiedDate());
		}
	};
	private static final Comparator<SearchResult> SORT_SEARCHS_BY_MAGIC = new Comparator<SearchResult>() {
		@Override
		public int compare(SearchResult o1, SearchResult o2) {
			
			if (Math.abs(o1.getScore() - o2.getScore()) >= 1)
				return Float.compare(o2.getScore(), o1.getScore());
			
			if (o1.getModifiedDate() == null || o2.getModifiedDate() == null)
				return 1;
			
			if (o1.getModifiedDate().equals(o2.getModifiedDate()))
				return Float.compare(o2.getScore(), o1.getScore());
			
			return o2.getModifiedDate().compareTo(o1.getModifiedDate());
		}
	};
	private static final Comparator<SearchResult> SORT_SEARCHS_BY_SCORE = new Comparator<SearchResult>() {
		@Override
		public int compare(SearchResult o1, SearchResult o2) {
			return Float.compare(o2.getScore(), o1.getScore());
		}
	};
	
	private Set<SearchResult> getSortedResults(Set<SearchResult> results) {
		
		Set<SearchResult> sorted;
		
		switch (sortSelection) {
		case date:
			sorted = new TreeSet<SearchResult>(SORT_SEARCHS_BY_DATE);
			break;
			
		case relevance:
			sorted = new TreeSet<SearchResult>(SORT_SEARCHS_BY_SCORE);
			break;
			
		case magic:
			sorted = new TreeSet<SearchResult>(SORT_SEARCHS_BY_MAGIC);
			break;
			
		default:
			sorted = new HashSet<SearchResult>();
			break;
		}
		
		sorted.addAll(results);
		
		return sorted;
	}
	
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		requestTimestamp = System.currentTimeMillis();
		
		if (verifyCredentials) {
			logger.debug("Cheking credentials ...");
			if (!verifyCredentials(request, response)) {
				return;
			}
		} else {
			logger.warn("Credentials not required...");
		}
		userid = (credentials != null ? credentials.getId() : "anonymous");
		
		logger.debug("Fetching request parameters...");
		fetchPageParameters(request);
		
		
		if (templateUtility == null) {
			logger.info("Initializing Template Utility");
			templateUtility = new TemplateUtility(super.getServletContext());
		}
		
		Set<SearchResult> results = new HashSet<SearchResult>();
		String queryText = "";
		
		logger.info("Creating SearchManager instance");
		SearchManager manager = SearchManagerFactory.getManager();
		
		
		if (searchInfo == null || searchInfo.length() == 0) {
			searchInfo = "";
		} else if (manager != null) {
			logger.debug("Creating query...");
			
			QueryBuilder<SimpleQuerySpec> builder = QueryBuilder.buildSimpleQuery();
			switch (filterTypeSelection) {
				case ticket:
				case wiki:
					builder.addStrongRestriction(filterTypeSelection.name(), TracStuffField.CONTEXT);
			}
			
			if (!recentTimeSelection.equals(RECENT_TIME_SELECTION.all_entries)) {
				builder.enableRecentFilter(recentTimeSelection.days);
			}
			
			switch (searchType) {
				case fast_search:
					builder.addWeakRestriction(searchInfo, TracStuffField.CONTENT);
					break;

				case lucene_search:
					builder.addLuceneRestriction(searchInfo);
					break;
					
				default:
				case normal_search:
					builder.addLikeRestriction(searchInfo, TracStuffField.CONTENT, TracStuffField.DESCRIPTION);
					break;
			}
						
			Query query = manager.buildQuery(builder.createQuerySpec());
						
			queryText = query.toString();
			
			logger.debug("Searching...");
			results = manager.doSearch(query, 100);
			
			logger.debug("Sorting results...");
			results = getSortedResults(results);
						
			AppConfiguration.getInstance().getIncrementQueryCount(userid);
		}
		
		logger.debug("Building page parameters...");
		VelocityContext context = buildPageResponse(results, queryText);
		
		// * Easter Egg *
		{
			final String eastereggkey = "trac-plus.easter-egg";
			final long userCount = AppConfiguration.getInstance().getQueryCountUser(userid);
			HttpSession session = request.getSession();
		
			if (session.getAttribute(eastereggkey) != null && searchInfo.equalsIgnoreCase("redxiii")) {
				context.put("trac_logo","images/redxiii-tracplus-logo.png");
			} else if ((userCount > 0 && userCount % 100 == 0) ||
				session.getAttribute(eastereggkey) != null) {
				session.setAttribute(eastereggkey, Boolean.TRUE);
				context.put("trac_logo","images/trac_plus_logo.png");
			} else {
				context.put("trac_logo","images/trac_logo.png");
			}
		}
		
		logger.debug("Printing page...");
		templateUtility.printPage("search.html", context, response.getWriter());
		
		logger.info("Done");
	}
	
	public VelocityContext buildPageResponse(Set<SearchResult> results, String queryText) {

		VelocityContext context = new VelocityContext();
		
		context.put("value", StringEscapeUtils.escapeHtml(searchInfo));
		
		for (FILTER_TYPE_SELECTION entry : FILTER_TYPE_SELECTION.values()) {
			if (entry.equals(filterTypeSelection))
				context.put("filter_type_" + entry.name() + "_checked","checked=\"checked\"");
			else
				context.put("filter_type_" + entry.name() + "_checked","");
		}
		
		for (RECENT_TIME_SELECTION entry : RECENT_TIME_SELECTION.values()) {
			if (entry.equals(recentTimeSelection))
				context.put(entry.name() + "_selected","selected=\"selected\"");
			else
				context.put(entry.name() + "_selected","");
		}
		
		
		
		for (SORT_SELECTION entry : SORT_SELECTION.values()) {
			if (entry.equals(sortSelection))
				context.put(entry.name() + "_selected","selected=\"selected\"");
			else
				context.put(entry.name() + "_selected","");
		}
		
		context.put("query", queryText);
		context.put("results",results);
		context.put("qtd",searchInfo.length() > 0 ? results.size() : -1);
		
		context.put("appversion",loadManifest());
		context.put("query-count-user", AppConfiguration.getInstance().getQueryCountUser(userid));
		context.put("query-count-month", AppConfiguration.getInstance().getQueryCountMonth());
		context.put("query-count-all", AppConfiguration.getInstance().getQueryCount());
		context.put("elapsed", (System.currentTimeMillis() - requestTimestamp) / 1000);
		context.put("loggeduser", userid);
		return context;
	}

	private boolean verifyCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException {
		
		credentials = (Credentials) request.getSession().getAttribute("credentials");
		if (credentials == null) {
			logger.warn("Access denied for ip: {}", request.getRemoteHost());
			response.sendRedirect(request.getContextPath() + "/Auth.html");
			return false;
		}
		
		String domain = AppConfiguration.getInstance().getAsString(AppParam.LOGIN_DOMAIN_AUTH);
		if (domain != null && !credentials.getEmail().endsWith(domain)) {
			logger.warn("Access denied for ip: {} / user: {}", request.getRemoteHost(), credentials.getEmail());
			response.sendRedirect(request.getContextPath() + "/Unauthorized.html");
			return false;
		}
		
		logger.info("Access granted for ip: {} / user: {}", request.getRemoteHost(), credentials.getEmail());
		return true;
	}
	
	private String loadManifest() {
		
		if (manifest == null) {
			StringBuilder builder = new StringBuilder("<h2>Trac+</h2>");
			Configuration configuration = new ManifestReader().getManifestProperties();
			if (configuration != null) {
				Iterator<?> keys = configuration.getKeys();
				while (keys.hasNext()) {
					String key = keys.next().toString();
					String value = configuration.getString(key);
					builder.append("<b>");
					builder.append(key);
					builder.append("</b>:   ");
					builder.append(value);
					builder.append("<br>");
				}
			}
			manifest = builder.toString();
		}
		return manifest;
	}
}
